<!DOCTYPE html>
<html lang="en">

<head>
	<title>three.js webgl - layers</title>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
	<link type="text/css" rel="stylesheet" href="/files/main.css">
	<style>
		body {
			background-color: #f0f0f0;
			color: #444;
		}

		a {
			color: #08f;
		}
	</style>
</head>

<body>

	<div id="info">
		<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - webgl layers
	</div>
	<script src='/build/three.js'></script>
	<script src='/examples/jsm/threex.dynamictexture.js'></script>
	<script src="https://cdn.jsdelivr.net/pako/1.0.3/pako.min.js"></script>

	<script type="module">
		// import * as THREE from '/build/three.module.js';
		import Stats from '/examples/jsm/libs/stats.module.js';
		import { GUI } from '/examples/jsm/libs/dat.gui.module.js';
		import { FlyControls } from '/examples/jsm/controls/FlyControls.js';
		import { OrbitControls } from '/examples/jsm/controls/OrbitControls.js';
		let container, stats;
		var camera, renderer;
		var controls;
		var scene;
		let theta = 0;
		const radius = 100;
		const clock = new THREE.Clock();
		// 历史轨迹
		var buf_storage = ''
		var core_L = [];
		var core_Obj = [];
		// global vars
		var play_fps = 10;
		var dt_since = 0;
		var dt_threshold = 1 / play_fps;
		var play_pointer = 0;
		var buf_str = '';
		var init_cam = false;
		var DEBUG = true;
		var transfer_ongoing = false;
		// GUI
		var request=null;
		var req_interval=2.0;
		var BarFolder;
		let panelSettings = {
				'play fps':play_fps,
				'play pointer':0,
				'data req interval': req_interval,
				'reset to read new': null
		};
		// color dictionary 
		var color_dic = {
			k:0x000000,
			r:0xff0000, 
			g:0x00ff00, 
			b:0x0000ff, 
		};
		var color_layer = {
			k:0,
			r:1, 
			g:2, 
			b:3, 
		};


		function toUint8Arr(str) {
            const buffer = [];
            for (let i = 0; i < str.length; i++) {
				buffer.push(str.charCodeAt(i)&0xff);
            }
            return Uint8Array.from(buffer);
		}

		function core_update(buf) {
			const EOFF = '>>v2d_show()\n'
			const EOFX = '>>v2d_init()\n'
			let tmp = buf.split(EOFF);

			// filter empty
			tmp = tmp.filter(function (s) {
				return s&&s.trim(); // '' --> exclude,  false--> exclude, true --> include
			});
			if (tmp.length==0){return 0;}


			if (buf_storage){
				tmp[0] = buf_storage + tmp[0]
			}
			buf_storage = ''
            if (!buf.endsWith(EOFF)){
				buf_storage = tmp.pop()
			}
			// if (tmp.length==0){return 0;}
			//check EOFX
			let eofx_i = -1;
			for (let i = 0; i < tmp.length; i++) {
				if (tmp[i].indexOf(EOFX) != -1){
					eofx_i = i
				}
			}
			if (eofx_i>=0){
				alert('new session detected')
				core_L = []
				for (let i = core_Obj.length-1; i>=0; i--) {
					scene.remove(core_Obj[i]);
				}
				core_Obj = []
				tmp.splice(0, eofx_i);
			}
			//
			core_L = core_L.concat(tmp);
			if (core_L.length>1e8){
				core_L.splice(0, tmp.length);
				play_pointer = play_pointer - tmp.length;
				if (play_pointer<0){
					play_pointer=0;
				}
			}
			BarFolder.__controllers[0].max(core_L.length);
			return tmp.length;
		}

		////////////////////////////////////////////////
		var coreReadFunc = function (auto_next=true) {
			if (transfer_ongoing){
				console.log('ongoing')
				req_interval = req_interval+1;
				panelSettings['data req interval'] = req_interval
				if (!DEBUG){setTimeout(coreReadFunc, req_interval*1000);}
				return
			}
			request = new XMLHttpRequest();
			request.open('POST', `/up`);
			request.overrideMimeType("text/plain; charset=x-user-defined");
			request.timeout = 30*1000; // 30sec 超时时间，单位是毫秒
			request.ontimeout = function (e) {
				transfer_ongoing = false;
				alert('xhr request timeout!')
			};
			request.onload = () => {
				console.log('loaded')
				let inflat;
				if (!DEBUG){
					let byteArray = toUint8Arr(request.response);
					inflat = pako.inflate(byteArray,{to: 'string'}); //window.pako.inflate(byteArray)
				}else{
					inflat = request.responseText
				}
				let n_new_data = core_update(inflat)
				if (n_new_data==0){
					req_interval = req_interval+1;
					if (req_interval>100){
						req_interval=100;
					}
					panelSettings['data req interval'] = req_interval
				}else{
					req_interval = 0
					panelSettings['data req interval'] = req_interval
				}

				transfer_ongoing = false;
			};
			request.send();
			console.log('send req')
			transfer_ongoing = true;
			if(auto_next && !DEBUG){setTimeout(coreReadFunc, req_interval*1000);}
			console.log('next update '+req_interval)
		}
		panelSettings['reset to read new'] = function (){
			if (transfer_ongoing){
				request.abort()
				transfer_ongoing = false;
			}
			coreReadFunc(false)
		}
		setTimeout(coreReadFunc, 100);
			// req_interval = setInterval(coreReadFunc, 1000);
		////////////////////////////////////////////////////////////

		
		function addCoreObj(my_id, color_str, geometry, x, y, z, geometry_size, currentSize, label_marking){
			const object = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({ color: color_dic[color_str] }));
			object.my_id = my_id;
			object.color_str = color_str;
			object.position.x = x; // -400 ~ 400
			object.position.y = y; // -400 ~ 400
			object.position.z = z; // -400 ~ 400
			object.next_pos = Object.create(object.position);
			object.prev_pos = Object.create(object.position);
			object.rotation.x = 0;  //Math.random() * 2 * Math.PI; // 0 ~ 360
			object.rotation.y = 0;  //Math.random() * 2 * Math.PI; // 0 ~ 360
			object.rotation.z = 0;  //Math.random() * 2 * Math.PI; // 0 ~ 360
			object.initialSize = geometry_size
			object.currentSize = currentSize
			object.generalSize = geometry_size

			object.scale.x = 1; //Math.random() + 0.5;
			object.scale.y = 1; //Math.random() + 0.5;
			object.scale.z = 1; //Math.random() + 0.5;
			object.label_marking = label_marking
			object.layers.set(color_layer[color_str]);

			if (!init_cam){
				console.log('first')
				init_cam=true;
				controls.target.set(object.position.x, -geometry_size, object.position.z); // 旋转的焦点在哪0,0,0即原点
				camera.position.set(object.position.x, geometry_size*100, object.position.z)
			}
			if (label_marking){
				makeClearText(object, object.label_marking)
			}
			scene.add(object);
			core_Obj.push(object)
		}

		function makeClearText(object, text, HWRatio=10){
			let textTextureHeight = 512
			let textTextureWidth = 512*HWRatio
			object.dynamicTexture  = new THREEx.DynamicTexture(textTextureWidth, textTextureHeight)
			let px = 256
			object.dynamicTexture.context.font	= `bolder ${px}px Verdana`
			object.dynamicTexture.drawText(text, 0, +80, 'black')	// text, x ,y, fillStyle（font color）, contextFont
			const materialB = new THREE.SpriteMaterial({ map:  object.dynamicTexture.texture, depthWrite: false });
			const sprite = new THREE.Sprite(materialB);

			sprite.scale.x = 17* object.generalSize
			sprite.scale.y = 17* object.generalSize/HWRatio
			sprite.scale.z = 17* object.generalSize
			sprite.position.set(object.generalSize, object.generalSize, -object.generalSize);
			object.add(sprite)
		}

		function removeEntity(object) {
			var selectedObject = scene.getObjectByName(object.name);
			scene.remove(selectedObject);
		}

		function changeCoreObjColor(object, color_str){
			const colorjs = color_dic[color_str];
			object.material.color.set(colorjs)
			object.layers.set(color_layer[color_str])
			object.color_str = color_str;
		}

		function changeCoreObjSize(object, size){
			let ratio_ = (size/object.initialSize)
			// console.log(size+'---'+object.initialSize+'---'+ratio_)
			// console.log(object.scale)
			object.scale.x = ratio_
			object.scale.y = ratio_
			object.scale.z = ratio_
			// console.log(object.scale)
			object.currentSize = size
		}

		// name, xpos, ypos, zpos, dir=0, **kargs
		function parse_update(buf_str) {
			let each_line = buf_str.split('\n')
			for (let i = 0; i < each_line.length; i++) {
				var str = each_line[i]
				if (str.search("v2dx") != -1) {
					// ">>v2dx(x, y, dir, xxx)"
					// each_line[i].replace('>>v2dx(')
					const pattern = />>v2dx\('(.*)',([^,]*),([^,]*),([^,]*),(.*)\)/
					let match_res = str.match(pattern)
					let name = match_res[1]
					// z --> y, y --- z
					let pos_x = parseFloat(match_res[2])
					let pos_y = parseFloat(match_res[4])
					let pos_z = parseFloat(match_res[3])
					// pattern.test(str)
					let name_split = name.split('|')
					let type = name_split[0]
					let my_id = parseInt(name_split[1])
					let color_str = name_split[2]
					let size = parseFloat(name_split[3])
					let label_marking = `id ${my_id}`
					// find hp 
					const hp_pattern = /health=([^,)]*)/
					let hp_match_res = str.match(hp_pattern)
					if (!(hp_match_res === null)){
						let hp = parseFloat(hp_match_res[1])
						if (Number(hp) === hp && hp % 1 === 0){
							// is an int
							hp = Number(hp);
						}
						else{
							hp = hp.toFixed(2);
						}
					 	label_marking = `HP ${hp}`
					}
					//// OTHERs
					// e.g. >>v2dx('tank|12|b|0.1',-8.09016994e+00,5.87785252e+00,0,vel_dir=0,health=0,label='12',attack_range=0)
					let res;
					// use label
					res = str.match(/label='(.*)'/)
					// console.log(res)
					if (!(res === null)){
						label_marking = res[1]
					}
					// find core obj by my_id
					let object = null
					for (let i = 0; i < core_Obj.length; i++) {
						if (core_Obj[i].my_id == my_id){
							object=core_Obj[i];
							break;
						}
					}
					// 已经创建了对象
					if (object) {
						object.prev_pos = Object.assign({}, object.next_pos);
						object.next_pos.x = pos_x; // -400 ~ 400
						object.next_pos.y = pos_y; // -400 ~ 400
						object.next_pos.z = pos_z; // -400 ~ 400
						if (color_str != object.color_str){
							changeCoreObjColor(object, color_str)
						}
						if (size != object.currentSize){
							changeCoreObjSize(object, size)
						}
						if (label_marking != object.label_marking){
							object.label_marking = label_marking
							if (!object.dynamicTexture){
								makeClearText(object, object.label_marking)
							}
							object.dynamicTexture.clear().drawText(label_marking, 0, -30, 'black')
						}
					}
					else {
						// create obj
						let geometry_size;
						let currentSize = size;
						if (size==0){
							geometry_size = 0.1
						}else{
							geometry_size = currentSize
						}
						let geometry = new THREE.BoxGeometry(geometry_size, geometry_size, geometry_size);
						//function addCoreObj(my_id, color_str, geometry, x, y, z, size, label_marking){
						addCoreObj(my_id, color_str, geometry, pos_x, pos_y, pos_z, geometry_size, currentSize, label_marking)
					}
				}
			}
		}

		function move_to_future(percent) {
			for (let i = 0; i < core_Obj.length; i++) {
				let object = core_Obj[i]
				object.position.x = object.prev_pos.x * (1 - percent) + object.next_pos.x * percent
				object.position.y = object.prev_pos.y * (1 - percent) + object.next_pos.y * percent
				object.position.z = object.prev_pos.z * (1 - percent) + object.next_pos.z * percent
			}
		}

		function set_next_play_frame() {
			if (core_L.length == 0) { return; }
			if (play_pointer >= core_L.length) {play_pointer = 0;}
			buf_str = core_L[play_pointer]
			parse_update(buf_str)
			play_pointer = play_pointer + 1
			if (play_pointer >= core_L.length) {play_pointer = 0;}
			panelSettings['play pointer'] = play_pointer;
		}

		function onWindowResize() {
			camera.aspect = window.innerWidth / window.innerHeight;
			camera.updateProjectionMatrix();
			renderer.setSize(window.innerWidth, window.innerHeight);
		}

		function animate() {
			requestAnimationFrame(animate);
			render();
			stats.update();
		}

		function render() {
			const delta = clock.getDelta();
			dt_since = dt_since + delta;
			if (dt_since > dt_threshold) {
				dt_since = 0;
				set_next_play_frame();
				move_to_future(0);
			}
			else {
				let percent = Math.min(dt_since / dt_threshold, 1.0);
				move_to_future(percent);
			}
			renderer.render(scene, camera);
		}


		function init() {
			container = document.createElement('div');
			document.body.appendChild(container);
			// 透视相机  Fov, Aspect, Near, Far – 相机视锥体的远平面
			camera = new THREE.PerspectiveCamera(80, window.innerWidth / window.innerHeight, 0.001, 10000);
			// camera.up.set(0,0,1);一个 0 - 31 的整数 Layers 对象为 Object3D 分配 1个到 32 个图层。32个图层从 0 到 31 编号标记。
			for (let i = 0; i < Object.keys(color_layer).length; i++) {
				camera.layers.enable(i); // 启动0图层
			}
			scene = new THREE.Scene();
			scene.background = new THREE.Color(0xa0a0a0);
			// ground
			// const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1000, 1000), 
			// new THREE.MeshPhongMaterial({ color: 0x999999, depthWrite: false })
			// );
			// mesh.rotation.x = - Math.PI / 2;
			// mesh.receiveShadow = true;
			// scene.add(mesh);
			const grid = new THREE.GridHelper( 500, 500, 0xffffff, 0x555555 );
			grid.position.y = 0
			scene.add(grid);
			const light = new THREE.PointLight(0xffffff, 1);
			for (let i = 0; i < Object.keys(color_layer).length; i++) {
				light.layers.enable(i); // 启动0图层
			}
			scene.add(camera);
			camera.add(light);

			renderer = new THREE.WebGLRenderer({ antialias: true });
			renderer.setPixelRatio(window.devicePixelRatio);
			renderer.setSize(window.innerWidth, window.innerHeight);
			container.appendChild(renderer.domElement);

			stats = new Stats();
			container.appendChild(stats.dom);

			controls = new OrbitControls(camera, renderer.domElement);
			// controls.object.up = new THREE.Vector3( 1, 0, 0 )
			controls.target.set(0, 0, 0); // 旋转的焦点在哪0,0,0即原点
			camera.position.set(0, 50, 0)
			controls.update();
			controls.autoRotate = false;

			
			const panel = new GUI( { width: 310 } );
			const Folder1 = panel.addFolder( 'General' );
			// FPS adjust
			Folder1.add(panelSettings, 'play fps', 0, 144, 1).onChange(
				function change_fps(fps) {
					play_fps = fps;
					dt_since = 0;
					dt_threshold = 1 / play_fps;
				});
			Folder1.add(panelSettings, 'data req interval', 1, 100, 1).listen().onChange(
				function (interval) {
					req_interval = interval;
			});
			Folder1.add( panelSettings, 'reset to read new' );
			Folder1.open();

			BarFolder = panel.addFolder('Play Pointer');
			BarFolder.add(panelSettings, 'play pointer', 0, 10000, 1).listen().onChange(
				function (p) {
					play_pointer = p;
					console.log('p changed')
			});
			BarFolder.open();

			window.addEventListener('resize', onWindowResize);
		}
		init();
		animate();
	</script>

</body>

</html>